package com.filippudak.progresspieview;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.element.Element;
import ohos.agp.render.Arc;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.TextAlignment;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayAttributes;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;

import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.utils.LruBuffer;

import com.filippudak.progresspieview.util.AttrUtils;
import com.filippudak.progresspieview.util.RectF;
import com.filippudak.progresspieview.util.ResUtils;

import java.util.Optional;

import static com.filippudak.progresspieview.util.Const.DEFAULT_MAX;
import static com.filippudak.progresspieview.util.Const.DEFAULT_PROGRESS;
import static com.filippudak.progresspieview.util.Const.DEFAULT_START_ANGLE;
import static com.filippudak.progresspieview.util.Const.DEFAULT_STROKE_WIDTH;
import static com.filippudak.progresspieview.util.Const.DEFAULT_TEXT_SIZE;
import static com.filippudak.progresspieview.util.Const.FILL_TYPE_CENTER;
import static com.filippudak.progresspieview.util.Const.FILL_TYPE_RADIAL;
import static com.filippudak.progresspieview.util.Const.MEDIUM_ANIMATION_SPEED;

public class ProgressPieView extends Component implements Component.DrawTask {

    public interface OnProgressListener {
        void onProgressChanged(int progress, int max);

        void onProgressCompleted();
    }

    private static LruBuffer<String, Font> sFontCache = new LruBuffer<String, Font>(8);

    private OnProgressListener mListener;
    private DisplayAttributes mDisplayMetrics;
    private int mMax = DEFAULT_MAX;
    private int mProgress = DEFAULT_PROGRESS;
    private int mStartAngle = DEFAULT_START_ANGLE;
    private boolean mInverted = false;
    private boolean mCounterclockwise = false;
    private boolean mShowStroke = true;
    private float mStrokeWidth = DEFAULT_STROKE_WIDTH;
    private boolean mShowText = true;
    private float mTextSize = DEFAULT_TEXT_SIZE;
    private String mText;
    private String mTypeface;
    private boolean mShowImage = true;
    private Element mImage;
    private Rect mImageRect;
    private Paint mStrokePaint;
    private Paint mTextPaint;
    private Paint mProgressPaint;
    private Paint mBackgroundPaint;
    private RectF mInnerRectF;
    private int mProgressFillType = FILL_TYPE_RADIAL;

    private int mAnimationSpeed = MEDIUM_ANIMATION_SPEED;
    private AnimationHandler mAnimationHandler = new AnimationHandler(EventRunner.getMainEventRunner());
    private int mViewSize;

    public ProgressPieView(Context context) {
        this(context, null);
    }

    public ProgressPieView(Context context, AttrSet attrSet) {
        super(context, attrSet);
        init(context, attrSet);
    }

    public ProgressPieView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init(context, attrSet);
    }

    public ProgressPieView(Context context, AttrSet attrSet, int resId) {
        super(context, attrSet, resId);
        init(context, attrSet);
    }

    private void init(Context context, AttrSet attrs) {
        addDrawTask(this);
        Optional<Display> dm = DisplayManager.getInstance().getDefaultDisplay(getContext());
        if (dm.isPresent()) {
            mDisplayMetrics = dm.get().getAttributes();
            mStrokeWidth = mStrokeWidth * mDisplayMetrics.densityPixels;
            mTextSize = mTextSize * mDisplayMetrics.scalDensity;
        }

        mMax = AttrUtils.getIntFromAttr(attrs, "ppvMax", mMax);
        mProgress = AttrUtils.getIntFromAttr(attrs, "ppvProgress", mProgress);
        mStartAngle = AttrUtils.getIntFromAttr(attrs, "ppvStartAngle", mStartAngle);
        String progressFillTypestr = AttrUtils.getStringFromAttr(attrs, "ppvProgressFillType", "radial");
        if (progressFillTypestr.equals("center"))
            mProgressFillType = 1;
        else
            mProgressFillType = 0;
        mInverted = AttrUtils.getBooleanFromAttr(attrs, "ppvInverted", mInverted);
        mCounterclockwise = AttrUtils.getBooleanFromAttr(attrs, "ppvCounterclockwise", mCounterclockwise);
        mShowStroke = AttrUtils.getBooleanFromAttr(attrs, "ppvShowStroke", mShowStroke);
        mShowText = AttrUtils.getBooleanFromAttr(attrs, "ppvShowText", mShowText);
        mStrokeWidth = AttrUtils.getDimensionFromAttr(attrs, "ppvStrokeWidth", (int) mStrokeWidth);
        mTextSize = AttrUtils.getDimensionFromAttr(attrs, "text_size", (int) mTextSize);
        mTypeface = AttrUtils.getStringFromAttr(attrs, "ppvTypeface", mTypeface);
        mText = AttrUtils.getStringFromAttr(attrs, "text", mText);
        mImage = AttrUtils.getElementFromAttr(attrs, "ppvImage", mImage);

        int backgroundColor = AttrUtils.getColorFromAttr(attrs, "ppvBackgroundColor", ResUtils.getColor(getContext(), ResourceTable.Color_default_background_color));
        int progressColor = AttrUtils.getColorFromAttr(attrs, "ppvProgressColor", ResUtils.getColor(getContext(), ResourceTable.Color_default_progress_color));
        int strokeColor = AttrUtils.getColorFromAttr(attrs, "ppvStrokeColor", ResUtils.getColor(getContext(), ResourceTable.Color_default_stroke_color));
        int textColor = AttrUtils.getColorFromAttr(attrs, "textColor", ResUtils.getColor(getContext(), ResourceTable.Color_default_text_color));

        mBackgroundPaint = new Paint();
        mBackgroundPaint.setAntiAlias(true);
        mBackgroundPaint.setColor(new Color(backgroundColor));
        mBackgroundPaint.setStyle(Paint.Style.FILL_STYLE);

        mProgressPaint = new Paint();
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setColor(new Color(progressColor));
        mProgressPaint.setStyle(Paint.Style.FILL_STYLE);

        mStrokePaint = new Paint();
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setColor(new Color(strokeColor));
        mStrokePaint.setStyle(Paint.Style.STROKE_STYLE);
        mStrokePaint.setStrokeWidth(mStrokeWidth);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(new Color(textColor));
        mTextPaint.setTextSize((int) mTextSize);
        mTextPaint.setTextAlign(TextAlignment.CENTER);

        mInnerRectF = new RectF();
        mImageRect = new Rect();
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        mViewSize = Math.min(getWidth(), getHeight());
        mInnerRectF = new RectF(0, 0, mViewSize, mViewSize);
        mInnerRectF.offset((getWidth() - mViewSize) / 2, (getHeight() - mViewSize) / 2);
        if (mShowStroke) {
            final int halfBorder = (int) (mStrokePaint.getStrokeWidth() / 2f + 0.5f);
            mInnerRectF.shrink(halfBorder, halfBorder);
        }
        float centerX = mInnerRectF.getCenter().getPointX();
        float centerY = mInnerRectF.getCenter().getPointY();

        canvas.drawArc(mInnerRectF, new Arc(0, 360, true), mBackgroundPaint);

        switch (mProgressFillType) {
            case FILL_TYPE_RADIAL:
                float sweepAngle = 360.0f * mProgress / mMax;
                if (mInverted) {
                    sweepAngle = sweepAngle - 360;
                }
                if (mCounterclockwise) {
                    sweepAngle = -sweepAngle;
                }
                canvas.drawArc(mInnerRectF, new Arc(mStartAngle, sweepAngle, true), mProgressPaint);
                break;
            case FILL_TYPE_CENTER:
                float radius = (float) (mViewSize / 2.0) * ((float) mProgress / mMax);
                if (mShowStroke) {
                    radius = radius + 0.5f - mStrokePaint.getStrokeWidth();
                }
                canvas.drawCircle(centerX, centerY, radius, mProgressPaint);
                break;
            default:
                throw new IllegalArgumentException("Invalid Progress Fill = " + mProgressFillType);
        }

        if (mText != null && mText.length() > 0 && mShowText) {
            if (mTypeface != null && mTypeface.length() > 0) {
                Font typeface = sFontCache.get(mTypeface);
                if (null == typeface) {
                    typeface = ResUtils.getFont(component.getContext(), mTypeface);
                    sFontCache.put(mTypeface, typeface);
                }
                mTextPaint.setFont(typeface);
            }
            int xPos = (int) centerX;
            int yPos = (int) (centerY - (mTextPaint.descent() + mTextPaint.ascent()) / 2);
            canvas.drawText(mTextPaint, mText, xPos, yPos);
        }

        if (null != mImage && mShowImage) {
            int drawableSize = mImage.getWidth();
            mImageRect.set(0, 0, drawableSize, drawableSize);
            mImageRect.translate((getWidth() - drawableSize) / 2, (getHeight() - drawableSize) / 2);//revisit
            mImage.setBounds(mImageRect);
            mImage.drawToCanvas(canvas);
        }

        if (mShowStroke) {
            canvas.drawOval(mInnerRectF, mStrokePaint);
        }
    }

    /**
     * Gets the maximum progress value.
     *
     * @return max progress value
     */
    public int getMax() {
        return mMax;
    }

    /**
     * Sets the maximum progress value. Defaults to 100.
     *
     * @param max max animation progress
     */
    public void setMax(int max) {
        if (max <= 0 || max < mProgress) {
            throw new IllegalArgumentException(
                    String.format("Max (%d) must be > 0 and >= %d", max, mProgress));
        }
        mMax = max;
        invalidate();
    }

    /**
     * Sets the animation speed used in the animateProgressFill method.
     *
     * @param animationSpeed animation speed
     */
    public void setAnimationSpeed(int animationSpeed) {
        this.mAnimationSpeed = animationSpeed;
    }

    /**
     * Returns the current animation speed used in animateProgressFill method.
     *
     * @return animation speed
     */
    public int getAnimationSpeed() {
        return this.mAnimationSpeed;
    }

    /**
     * Animates a progress fill of the view, using a Handler.
     */
    public void animateProgressFill() {
        mAnimationHandler.removeEvent(0);
        mAnimationHandler.setAnimateTo(mMax);
        mAnimationHandler.sendEvent(0);
        invalidate();
    }

    /**
     * Animates a progress fill of the view, using a Handler.
     *
     * @param animateTo - the progress value the animation should stop at (0 - MAX)
     */
    public void animateProgressFill(int animateTo) {
        mAnimationHandler.removeEvent(0);
        if (animateTo > mMax || animateTo < 0) {
            throw new IllegalArgumentException(
                    String.format("Animation progress (%d) is greater than the max progress (%d) or lower than 0 ", animateTo, mMax));
        }
        mAnimationHandler.setAnimateTo(animateTo);
        mAnimationHandler.sendEvent(0);
        invalidate();
    }

    /**
     * Stops the views animation.
     */
    public void stopAnimating() {
        mAnimationHandler.removeEvent(0);
        mAnimationHandler.setAnimateTo(mProgress);
        invalidate();
    }

    /**
     * Gets the current progress from 0 to max.
     *
     * @return progress value
     */
    public int getProgress() {
        return mProgress;
    }

    /**
     * Sets the current progress (must be between 0 and max).
     *
     * @param progress progress in percentage
     */
    public void setProgress(int progress) {
        if (progress > mMax || progress < 0) {
            throw new IllegalArgumentException(
                    String.format("Progress (%d) must be between %d and %d", progress, 0, mMax));
        }
        mProgress = progress;
        if (null != mListener) {
            if (mProgress == mMax) {
                mListener.onProgressCompleted();
            } else {
                mListener.onProgressChanged(mProgress, mMax);
            }
        }
        invalidate();
    }

    /**
     * Gets the start angle the {@link #FILL_TYPE_RADIAL} uses.
     *
     * @return start angle value
     */
    public int getStartAngle() {
        return mStartAngle;
    }

    /**
     * Sets the start angle the {@link #FILL_TYPE_RADIAL} uses.
     *
     * @param startAngle start angle in degrees
     */
    public void setStartAngle(int startAngle) {
        mStartAngle = startAngle;
    }

    /**
     * Gets the inverted state.
     *
     * @return true if inverted
     */
    public boolean isInverted() {
        return mInverted;
    }

    /**
     * Sets the inverted state.
     *
     * @param inverted draw the progress inverted or not
     */
    public void setInverted(boolean inverted) {
        mInverted = inverted;
    }

    /**
     * Gets the counterclockwise state.
     *
     * @return true if rotation is counter clockwise
     */
    public boolean isCounterclockwise() {
        return mCounterclockwise;
    }

    /**
     * Sets the counterclockwise state.
     *
     * @param counterclockwise draw the progress counterclockwise or not
     */
    public void setCounterclockwise(boolean counterclockwise) {
        mCounterclockwise = counterclockwise;
    }

    /**
     * Gets the color used to display the progress of the view.
     *
     * @return progress color set
     */
    public int getProgressColor() {
        return mProgressPaint.getColor().getValue();
    }

    /**
     * Sets the color used to display the progress of the view.
     *
     * @param color - color of the progress part of the view
     */
    public void setProgressColor(int color) {
        mProgressPaint.setColor(new Color(color));
        invalidate();
    }

    /**
     * Gets the color used to display the background of the view.
     *
     * @return background color
     */
    public int getBackgroundColor() {
        return mBackgroundPaint.getColor().getValue();
    }

    /**
     * Sets the color used to display the background of the view.
     *
     * @param color - color of the background part of the view
     */
    public void setBackgroundColor(int color) {
        mBackgroundPaint.setColor(new Color(color));
        invalidate();
    }

    /**
     * Gets the color used to display the text of the view.
     *
     * @return text color
     */
    public int getTextColor() {
        return mTextPaint.getColor().getValue();
    }

    /**
     * Sets the color used to display the text of the view.
     *
     * @param color - color of the text part of the view
     */
    public void setTextColor(int color) {
        mTextPaint.setColor(new Color(color));
        invalidate();
    }

    /**
     * Gets the text size in sp.
     *
     * @return text size
     */
    public float getTextSize() {
        return mTextSize;
    }

    /**
     * Sets the text size.
     *
     * @param sizeSp in sp for the text
     */
    public void setTextSize(int sizeSp) {
        mTextSize = sizeSp * mDisplayMetrics.scalDensity;
        mTextPaint.setTextSize((int) mTextSize);
        invalidate();
    }

    /**
     * Gets the text of the view.
     *
     * @return display text
     */
    public String getText() {
        return mText;
    }

    /**
     * Sets the text of the view.
     *
     * @param text to be displayed in the view
     */
    public void setText(String text) {
        mText = text;
        invalidate();
    }

    /**
     * Gets the typeface of the text.
     *
     * @return the type face
     */
    public String getTypeface() {
        return mTypeface;
    }

    /**
     * Sets the text typeface.
     * - i.e. fonts/Roboto/Roboto-Regular.ttf
     *
     * @param typeface that the text is displayed in
     */
    public void setFontTypeface(String typeface) {
        mTypeface = typeface;
        invalidate();
    }

    /**
     * Gets the show text state.
     *
     * @return true if Text is showing
     */
    public boolean isTextShowing() {
        return mShowText;
    }

    /**
     * Sets the show text state.
     *
     * @param showText show or hide text
     */
    public void setShowText(boolean showText) {
        mShowText = showText;
        invalidate();
    }

    /**
     * Get the color used to display the stroke of the view.
     *
     * @return Stroke color
     */
    public int getStrokeColor() {
        return mStrokePaint.getColor().getValue();
    }

    /**
     * Sets the color used to display the stroke of the view.
     *
     * @param color - color of the stroke part of the view
     */
    public void setStrokeColor(int color) {
        mStrokePaint.setColor(new Color(color));
        invalidate();
    }

    /**
     * Gets the stroke width in dp.
     *
     * @return Stroke width
     */
    public float getStrokeWidth() {
        return mStrokeWidth;
    }

    /**
     * Set the stroke width.
     *
     * @param widthDp in dp for the pie border
     */
    public void setStrokeWidth(int widthDp) {
        mStrokeWidth = widthDp * mDisplayMetrics.densityPixels;
        mStrokePaint.setStrokeWidth(mStrokeWidth);
        invalidate();
    }

    /**
     * Gets the show stroke state.
     *
     * @return is Stroke Showing
     */
    public boolean isStrokeShowing() {
        return mShowStroke;
    }

    /**
     * Sets the show stroke state.
     *
     * @param showStroke show or hide stroke
     */
    public void setShowStroke(boolean showStroke) {
        mShowStroke = showStroke;
        invalidate();
    }

    /**
     * Gets the element of the component.
     *
     * @return get Image set
     */
    public Element getImageDrawable() {
        return mImage;
    }

    /**
     * Sets the drawable of the view.
     *
     * @param image drawable of the view
     */
    public void setImageDrawable(Element image) {
        mImage = image;
        invalidate();
    }

    /**
     * Sets the drawable of the view.
     *
     * @param resId resource id of the view's drawable
     */
    public void setImageResource(int resId) {
        mImage = ResUtils.getPixelElementByResId(getContext(), resId);
        invalidate();
    }

    /**
     * Gets the show image state.
     *
     * @return show image value
     */
    public boolean isImageShowing() {
        return mShowImage;
    }

    /**
     * Sets the show image state.
     *
     * @param showImage show or hide image
     */
    public void setShowImage(boolean showImage) {
        mShowImage = showImage;
        invalidate();
    }

    /**
     * Gets the progress fill type.
     *
     * @return get Progress fill Type FILL_TYPE_RADIAL or FILL_TYPE_CENTER
     */
    public int getProgressFillType() {
        return mProgressFillType;
    }

    /**
     * Sets the progress fill type.
     *
     * @param fillType one of {@link #FILL_TYPE_CENTER}, {@link #FILL_TYPE_RADIAL}
     */
    public void setProgressFillType(int fillType) {
        mProgressFillType = fillType;
    }

    /**
     * Sets the progress listner.
     *
     * @param listener progress listener
     * @see com.filippudak.progresspieview.ProgressPieView.OnProgressListener
     */
    public void setOnProgressListener(OnProgressListener listener) {
        mListener = listener;
    }

    /**
     * Handler used to perform the fill animation.
     */
    private class AnimationHandler extends EventHandler {

        private int mAnimateTo;

        public AnimationHandler(EventRunner runner) throws IllegalArgumentException {
            super(runner);
        }

        public void setAnimateTo(int animateTo) {
            mAnimateTo = animateTo;
        }

        @Override
        protected void processEvent(InnerEvent event) {
            if (mProgress > mAnimateTo) {
                setProgress(mProgress - 1);
                sendEvent(0, mAnimationSpeed);
            } else if (mProgress < mAnimateTo) {
                setProgress(mProgress + 1);
                sendEvent(0, mAnimationSpeed);
            } else {
                removeEvent(0);
            }
        }
    }
}